﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the Microsoft Public License (Ms-PL).
// See accompanying file License.txt or copy at http://mews.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Mews.Application
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Security.Cryptography;
    using System.Threading;
    using System.Windows.Forms;

    using Mews.Application.Properties;
    using Mews.Net;
    using Mews.Net.Nntp;
    using Mews.Net.Nntp.Cache;

    /// <summary>The main form of the application.</summary>
    internal sealed partial class MainForm : Form, ISendReceiveProgressObserver
    {
        private readonly string tempDirectory;
        private readonly Cache cache;
        private readonly Action<bool> update;

        private readonly IDictionary<IGroup, int> groupThreadCounts = new Dictionary<IGroup, int>();
        private IDictionary<SendStatus, int> outboundArticleCounts = new Dictionary<SendStatus, int>();
        private IController controller;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnConnecting"/>.</summary>
        public IDisposable OnConnecting(string hostName)
        {
            this.SetStatusAsync(Format(Resources.ConnectingTo, hostName));

            try
            {
                return new NetworkConnection(hostName, 60000);
            }
            catch (NetworkConnectionFailedException ex)
            {
                this.ShowErrorAsync(ConnectionFailedHelper.GetStatusMessage(ex.Status));
                return null;
            }
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnConnected"/>.</summary>
        public void OnConnected(string hostName)
        {
            this.SetStatusAsync(Format(Resources.ConnectedTo, hostName));
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnSendProgressChanged"/>.</summary>
        public void OnSendProgressChanged(int percent)
        {
            this.SetStatusAsync(Format(Resources.SendProgress, percent));
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnHeadersProgressChanged"/>.</summary>
        public void OnHeadersProgressChanged(string groupDisplayName, int percent)
        {
            this.SetStatusAsync(Format(Resources.HeadersProgress, groupDisplayName, percent));
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnBodiesProgressChanged"/>.</summary>
        public void OnBodiesProgressChanged(string groupDisplayName, int percent)
        {
            this.SetStatusAsync(Format(Resources.BodiesProgress, groupDisplayName, percent));
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnFinished"/>.</summary>
        public void OnFinished()
        {
            Thread.Sleep(1000); // Allow previous status to remain displayed briefly
            this.SetStatusAsync(Resources.SendReceiveFinished);
            Thread.Sleep(1000);
            this.BeginInvoke(this.update, true);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnStopped"/>.</summary>
        public void OnStopped()
        {
            this.SetStatusAsync(Resources.SendReceiveStopped);
        }

        /// <summary>See <see cref="ISendReceiveProgressObserver.OnError"/>.</summary>
        public bool OnError(string hostName, string groupName, Exception exception)
        {
            return
                TryHandle<PostingNotPermittedException>(
                    exception, Resources.SendReceiveErrorPostingNotPermitted, hostName) ||
                TryHandle<PostingFailedException>(
                    exception, Resources.SendReceiveErrorPostingFailed, hostName) ||
                TryHandle<UnknownCommandException>(
                    exception, Resources.SendReceiveErrorUnknownCommand, hostName) ||
                TryHandle<ServiceUnavailableException>(
                    exception, Resources.ErrorServiceUnavailable, hostName) ||
                TryHandle<ServerFaultException>(
                    exception, Resources.SendReceiveErrorServerFault, hostName, groupName) ||
                TryHandle<NoSuchGroupException>(
                    exception, Resources.SendReceiveErrorGroupUnknown, hostName, groupName) ||
                TryHandle<MissingAuthenticationException>(
                    exception, Resources.SendReceiveErrorAuthenticationMissing, hostName, groupName) ||
                TryHandle<AuthenticationFailedException>(
                    exception, Resources.ErrorAuthenticationFailed, hostName) ||
                TryHandle<CryptographicException>(
                    exception, Resources.DecryptionFailed, hostName) ||
                TryHandle<MissingPrivacyException>(
                    exception, Resources.SendReceiveErrorPrivacyMissing, hostName, groupName) ||
                TryHandle<UnexpectedResponseException>(
                    exception, Resources.SendReceiveErrorResponseUnexpected, hostName, groupName) ||
                TryHandle<HostUnreachableException>(
                    exception, Resources.ErrorHostUnreachable, hostName) ||
                TryHandle<ConnectionLostException>(
                    exception, Resources.SendReceiveErrorConnectionLost, hostName, groupName) ||
                TryHandle<WriteFailedException>(
                    exception, Resources.SendReceiveErrorWriteFailed);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal MainForm(string tempDirectory, Cache cache)
        {
            this.tempDirectory = tempDirectory;
            this.cache = cache;
            this.update =
                refreshAllGroupThreadCounts => this.ExecuteAndUpdate(() => { }, refreshAllGroupThreadCounts);

            this.InitializeComponent();

            this.treeView.ImageList =
                (this.CurrentAutoScaleDimensions.Height >= 192) ? this.article32ImageList : this.article16ImageList;
            this.controller = new NullController(this);
            this.update(true);
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="Control.Dispose"/>.</summary>
        protected sealed override void Dispose(bool disposing)
        {
            try
            {
                if (disposing)
                {
                    this.controller.Dispose();

                    if (this.components != null)
                    {
                        this.components.Dispose();
                    }
                }
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private void OnStatusTextBox_GotFocus(object sender, EventArgs e)
        {
            this.treeView.Focus();
        }

        ////////// MainMenu handlers //////////

        private void OnMainMenuItem_Popup(object sender, EventArgs e)
        {
            // Send/Receive
            this.sendReceiveMenuItem.MenuItems.Clear();

            if (this.cache.IsSendReceiveInProgress)
            {
                this.sendReceiveMenuItem.Text = Resources.StopSendReceive;
            }
            else
            {
                this.sendReceiveMenuItem.Text = Resources.SendReceive;
                this.sendReceiveMenuItem.MenuItems.Add(this.sendReceiveMinimalMenuItem);
                this.sendReceiveMenuItem.MenuItems.Add(this.sendReceiveFullMenuItem);
            }

            // Go To
            this.goToMenuItem.MenuItems.Clear();

            foreach (var groupThreadCount in this.groupThreadCounts)
            {
                // Make sure a different local variable is captured for every added handler
                var group = groupThreadCount.Key;
                var threadCount = groupThreadCount.Value;
                var menuItem = new MenuItem();
                menuItem.Text =
                    group.DisplayName +
                    (threadCount > 0 ? " (" + threadCount.ToString(CultureInfo.CurrentCulture) + ")" : string.Empty);
                menuItem.Checked = group.Equals(this.controller.Document);
                menuItem.Click +=
                    (sender2, e2) => this.ChangeController(() => new GroupController(this, group, this.tempDirectory));
                this.goToMenuItem.MenuItems.Add(menuItem);
            }

            var myArticlesMenuItem = new MenuItem() { Text = Resources.MyArticles };
            this.goToMenuItem.MenuItems.Add(myArticlesMenuItem);

            foreach (var pair in this.outboundArticleCounts)
            {
                var sendStatus = pair.Key;
                var text = GetFolderName(sendStatus) +
                    ((sendStatus == SendStatus.Sent) || (pair.Value == 0) ? string.Empty : " (" + pair.Value + ")");
                var menuItem = new MenuItem() { Text = text, Checked = sendStatus.Equals(this.controller.Document) };
                menuItem.Click +=
                    (sender2, e2) => this.ChangeController(() => new MyArticlesController(this, sendStatus));
                myArticlesMenuItem.MenuItems.Add(menuItem);
            }

            var deletedMenuItem = new MenuItem()
            {
                Text = Resources.Deleted,
                Checked = object.ReferenceEquals(this, this.controller.Document)
            };

            deletedMenuItem.Click +=
                (sender2, e2) => this.ChangeController(() => new MyDeletedArticlesController(this, this));
            myArticlesMenuItem.MenuItems.Add(deletedMenuItem);
        }

        private void OnExitMenuItem_Click(object sender, EventArgs e)
        {
            Cursor.Current = Cursors.WaitCursor;

            try
            {
                this.cache.StopSendReceive();
                this.Close();
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
        }

        private void OnQuickStartMenuItem_Click(object sender, EventArgs e)
        {
            FormHelper.ShowDialog(new ManualForm(this, "QuickStart.htm"));
        }

        private void OnManualMenuItem_Click(object sender, EventArgs e)
        {
            FormHelper.ShowDialog(new ManualForm(this, "Manual.htm"));
        }

        private void OnAboutMenuItem_Click(object sender, EventArgs e)
        {
            FormHelper.ShowDialog(new AboutForm(this));
        }

        private void OnSendReceiveMenuItem_Click(object sender, EventArgs e)
        {
            this.cache.StopSendReceive();
        }

        private void OnSendReceiveAllMenuItem_Click(object sender, EventArgs e)
        {
            this.cache.StartSendReceive(
                this.groupThreadCounts.Select(pair => pair.Key).ToArray(),
                ((MenuItem)sender).Parent == this.sendReceiveFullMenuItem,
                this);
        }

        private void OnOptionsMenuItem_Click(object sender, EventArgs e)
        {
            this.controller.Dispose();
            this.controller = new NullController(this); // The current group might be deleted
            FormHelper.ShowDialog(new OptionsForm(this, this.cache));
            this.update(true);
        }

        ////////// Helper methods //////////

        private void SetStatusAsync(string text)
        {
            this.BeginInvoke(new Action(() => this.statusTextBox.Text = text));
        }

        private void ShowErrorAsync(string text)
        {
            this.BeginInvoke(new Action(() => MessageBoxHelper.ShowError(text, Resources.SendReceiveErrorCaption)));
        }

        private bool TryHandle<TException>(
            Exception exception, string format, params object[] args) where TException : Exception
        {
            bool result = exception is TException;

            if (result)
            {
                this.ShowErrorAsync(Format(format, args) + Environment.NewLine + exception.Message);
            }

            return result;
        }

        private void ExecuteAndUpdate(Action action, bool refreshAllGroupThreadCounts)
        {
            Cursor.Current = Cursors.WaitCursor;
            this.treeView.BeginUpdate();

            try
            {
                action();

                if (refreshAllGroupThreadCounts)
                {
                    this.groupThreadCounts.Clear();

                    foreach (var host in this.cache.GetHosts())
                    {
                        foreach (var group in host.GetGroups())
                        {
                            if (group.Equals(this.controller.Document))
                            {
                                this.groupThreadCounts.Add(group, 0); // Dummy, will be updated below
                            }
                            else
                            {
                                // Surprisingly, it appears that a single query requires roughly 500kB of memory,
                                // which seems rather excessive when compared to the speed gain of less than one
                                // second per query. We therefore only keep a cache for the current group and
                                // destroy all other caches immediately.
                                using (var queryCache = group.CreateQueryCache())
                                {
                                    this.groupThreadCounts.Add(queryCache.Group, queryCache.GetThreadCount());
                                }
                            }
                        }
                    }

                    this.UpdateOutboundArticleCounts();
                }

                this.controller.UpdateCount();
                this.controller.Update();
            }
            finally
            {
                this.treeView.EndUpdate();
                Cursor.Current = Cursors.Default;
            }

            if (this.treeView.SelectedNode != null)
            {
                // Apparently, this needs to be done *after* EndUpdate, otherwise there's no effect
                this.treeView.SelectedNode.EnsureVisible();
            }
        }

        private void UpdateOutboundArticleCounts()
        {
            this.outboundArticleCounts = this.cache.GetOutboundArticleCounts();
        }

        private void ChangeController(Func<IController> createController)
        {
            this.controller.UpdateCount();
            this.controller.Dispose();
            this.controller = createController();
            this.update(false);
        }

        private static string GetFolderName(SendStatus sendStatus)
        {
            return Resources.ResourceManager.GetString(sendStatus.ToString(), Resources.Culture);
        }

        private static string Format(string format, params object[] args)
        {
            return string.Format(CultureInfo.InvariantCulture, format, args);
        }
    }
}
